この章では、スキャナ定義の構成要素を説明し、その使用例を示します。Flex を効率的に使用するためには、定義の個々の要素を完全に理解することが非常に重要です。したがって、初めて Flex を使うユーザには、時間をかけてこの章を読むことをお薦めします。
Flex スキャナ定義のほとんどの要素は必須要素ではありません。全体的な定義フォーマットは以下のようになります。
C のコードが記述できるところにはどこにでもコメントを記述することができます。コメントの書式は、C のコメントの規則に従います。コメントは、記述情報に影響を与えることはありません。C スタイルのコメントは以下のようになります。
注:C 以外の言語(例えば Pascal)のコードを生成する Lex が存在します。このような Lex ではコメントの書式はおそらく異なるものでしょう。Flex の場合は C のコードしか生成しません。
プログラマは2つの異なる方法を用いてスキャナの中に直接 C のコードを含めることができます。第1の方法は、「定義、初期 C コード」セクション(最初の %% より前の部分)にコードを含めることです。第2の方法は、「他の C コード」セクション(2番目の %% よりうしろの部分)にコードを含めることです。どちらの場合もコードはそのまま lex.yy.c にコピーされますので、正当なコードでなければなりません。
第1のセクション中の C コードは以下の形式になります。
C のコードが最初のカラムから始まるのでなければ、%{ と %} のペアは必要ありません。しかし普通は、分りやすくするためにこれらを記述しておいた方が良いでしょう。もう1つのポイントは、#ifdef 等のように最左端のカラムから始まらなければならず、かつ、通常はスキャナ記述情報の先頭に置かれる必要のあるものが存在するという点です。このようなものが %{ と %} のペアに囲まれていないと、Flex はそれを定義の一部であると考えるでしょう。これが、常に %{ と %} を使うもう1つの理由です。
最後の(「他の C コード」)セクション内のコードはそのままコピーされます。ここには特別な宣言は必要ありません。
定義セクションにおいてプログラマは、ある文字のグループに一意な識別子を与え、その識別子がその文字グループに置き換えられるようにすることができます。定義は以下のような形式になります。
Flex と Lex との非常に重要な相違点に、定義を展開する時 Flex はそれを文字どおりに丸括弧 () で囲むのに対して、Lex は囲まないという点があります。これは ^、<<EOF>>、$、/ を定義中に入れることができないということを意味しています。というのは、これらの文字は丸括弧 () で囲まれた部分に入れることができないからです。これについては、***ページの 3.6.1 節「文字」および***ページの 8 章「Flex と Lex」において、より詳細に説明されます。
例えば、
2つのパーセント記号が、スキャナ記述情報のルール・セクションの先頭と末尾を示します。すべての Flex 記述情報は、少なくともルール・セクションの先頭を示す %% を含んでいなければなりません。
ルールは Flex の心臓部です。ルールを書くことによってプログラマは、スキャナが何を実行するべきであるかを Flex に通知します。
通常、ルールは2つの部分から構成されます。
スキャナがマッチするものを2つ以上見つけた場合、以下の2つのルールを使ってどれを受け入れるかを決めます。
注:ほとんどのバージョンの Lex は { と } のペアの外部では単一の文しか許しません(例えば上の sayonara ルールは許されません)。また、C 以外の言語をターゲットにしている Lex では { と } のペアは、例えば Pascal の場合の begin と end のように、異なるシンボルに置き換える必要があるかもしれません。
ルールにマッチしなかった入力に対するデフォルトのアクションはそれを stdout に出力することで、マッチしたパターンに対するデフォルトのアクションはそれを破棄することであるという点に注意してください。これは、最も単純な Flex の定義が
パターン・セクションは、正規表現と呼ばれる仕組みを使って実際のマッチング処理を実行します。正規表現は、文字列、文字、文字集合(クラス)、演算子から構成されています。正規表現を構成する要素については次節以降で説明します。また正規表現自体については***ページの 3,7 節「正規表現」において議論します。
いくつかの文字は Flex にとって特別の意味があり、それらの文字を単独で使ったのでは、それらの文字自体を表すことができません。以下に
Flex における特殊文字とその意味を表にして示します。
文字 | Flex による解釈 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
. | ピリオドは改行(\n)以外の任意の文字を表します。
|
||||||||||||
\ | バックスラッシュはエスケープ文字です。エスケープ・シーケンスは ANSI
C のものと同一です。
|
||||||||||||
[] | 角括弧 [] は複数の文字を文字クラスにグループ化します。詳細については***ページの
3.6.3 節「Flex における文字のグループ化」を参照してください。
|
||||||||||||
^ | 文字クラスの内部では ^ は否定を意味します。詳細については***ページの
3.6.3 節「Flex における文字のグループ化」を参照してください。文字クラスの外部では、それは行の先頭を意味し、ルールの先頭にのみ置くことができます。例を以下に示します。
|
||||||||||||
- | ハイフンは文字クラスの内部において文字の範囲を表します。文字クラスの外部では、ハイフンはそれ自身を表します。詳細については***ページの
3.6.3 節「Flex における文字のグループ化」を参照してください。
|
||||||||||||
{} | 大括弧 {} は、定義の参照、複数行のアクションの囲い、ある範囲にわたる繰り返しの定義を行います。例を挙げると、定義
FOO があって、それをルールの中で参照したい場合に {FOO} を使います。
与えられたパターンのある範囲にわたる繰り返しを定義するには以下のような {repetition list} を使います。
f{2,5} /* f の2回以上5回以下の繰り返しにマッチ */ f{2,} /* f の2回以上の繰り返しにマッチ */ f{2} /* f の2回の繰り返しにマッチ */ |
||||||||||||
() | 丸括弧 () を使って優先順位を変更することができます。また、定義が展開される時には、その定義は暗黙のうちに丸括弧
() で囲まれることに注意してください。このため Lex と非互換なところがでてきます。この点については、***ページの
8.1.1 節「Flex と POSIX」と***ページの
3.3 節「定義」において説明されています。
|
||||||||||||
"" | 二重引用符記号は文字列を表します。引用符の内側の文字列だけがマッチの対象になります。したがって、
"string" |
||||||||||||
/ | スラッシュは後続コンテキスト(trailing context)を設定します。これは、あるパターンのうしろに特定の文字の並びが続く場合のみ、そのパターンを認識したいような状況です。つまりスラッシュは「その先を見よ(lookahead)」というような演算子として機能するということです。例を挙げると、
注:1つのルールの中では / は1つだけ許されます。つまり、abc/def/hijkl は不正です。 |
||||||||||||
<> | かぎ括弧 <> はスタート状態を参照します。それは EOF シンボル(<<EOF>>)にも使われます。これに関する完全な説明については、***ページの
3.8 節「スタート状態」と***ページの 5.6 節「ファイルの終端(End-Of-File)ルール」を参照してください。
|
||||||||||||
?+* | ?、+、* は、ある正規表現が現れることのできる回数を設定します。? は0回もしくは1回(その正規表現が現れることは必須ではないということ)を意味します。+
は1回以上を意味します。* は0回以上を意味します。例えば、
|
||||||||||||
| | OR 演算子、および、特別なアクションを表します。例えば、
oranges printf("fruit!\n"); |
||||||||||||
$ | ドル記号は行末を意味します。例えば、
|
Flex には3種類のエスケープ・シーケンスがあります。バックスラッシュ \ に続けて 8 進数を使うもの、\x に続けて 16 進数を使うもの、\letter という表記法で文字 letter もしくは特別な表示不可の文字を表すものの3つです。C をよく知っている人であれば、これらが ANSI C のエスケープ・シーケンスであることに気がつくことでしょう。数値によるエスケープ・シーケンスは、100 パーセント移植性があるわけではなく、保守を困難にするので、避けるべきです。
以下に、文字の使用に関する要約を示します。この表中では、c が単一の文字を、NNN
が 8 進定数を、HH が 16 進定数を表します。
文字 | 意味 | 例 |
---|---|---|
c | c が演算子でない場合は、文字 c 自体 | a |
"c" | c | "|" |
. | 改行以外の任意の文字 | Un.x |
\b | バックスペース(BS) | \b |
\t | 水平タブ(HT) | \t |
\n | 改行(NL) | \n |
\v | 垂直タブ(VT) | \v |
\f | 頁送り(FF) | \f |
\r | キャリッジ・リターン(CR) | \r |
\" | 単一引用符 | \" |
\\ | 単一バックスラッシュ | \\ |
\0 | NUL 文字 | \0 |
\c | 上記以外の文字 c については文字 c そのものを表す | \* |
\xHH | 16 進数 HH を値として持つ文字 | \x1B |
\NNN | 8 進数 NNN を値として持つ文字 | \033 |
注:いくつかのバージョンの Lex は \0 を正しく認識もしくはマッチしません。これは、\0 が NUL、つまり C 文字列の終端文字だからです。NUL をマッチの対象にすることは Flex では問題になりませんが、性能には若干影響します。
さらに加えると、^ 演算子と <<EOF>> はルールの先頭にのみ置くことができます。また、これらと $、/ は丸括弧 () の内部に置くことはできません。このことはまた、特定の定義の正当性に影響を及ぼします。というのは、展開される時に定義は文字どおりに丸括弧 () で囲まれるからです。詳細については、***ページの 3.3 節「定義」と***ページの 8.1.1 節「Flex と POSIX」を参照してください。
文字列とは(常に、というわけではありませんが)多くの場合引用符によって囲まれる、文字のグループです。エスケープ・シーケンスが使われない限り、文字列は改行や表示不可の文字を含むことはできません。
-i オプション(詳細については***ページの 5.1 節「大文字・小文字を区別しないスキャナ」を参照)を使わない限り、大文字・小文字の区別も含めた字義どおりの文字列に対してマッチが行われます。引用符付きの文字列については、引用符は認識される文字列には含まれません。
例えば、
Flex では、文字をグループ化して文字クラスにすることができます。文字クラスは文字のグループを角括弧 [] で囲むことにより作成されます。どのような文字でも正当です(表示不可の文字についてはエスケープ・シーケンスを使います)。また、文字の範囲をハイフン - を使って指定することができます。文字クラスがルールの中で使われている場合には、Flex はそのクラスの任意のメンバとマッチさせ、あたかも単一文字が使われているかのように振舞います。例えば、
否定文字クラスを表す正規表現を書くこともできます。否定文字クラスは(\n
も含めて)文字クラスのメンバ以外であれば何にでもマッチします。これをするには、否定すべきクラスの先頭に
^ を置きます(クラスの外部では ^ は異なる意味を持つことに注意してください)。以下に、正当なクラスの例をいくつか挙げます。
[abc] | a、b、c にマッチします。
|
[abc\n] | a、b、c、\n にマッチします。
|
[a-z] | ASCII 値が a から z までの範囲にある任意の文字にマッチします。これは任意の英小文字にマッチするということです。
|
[^a-z] | a から z までの範囲にある文字以外の任意の文字にマッチします。
|
[ABcd] | A、B、c、d にマッチします。
|
Flex の文字、文字列、クラス、定義、演算子を組み合わせることで、正規表現として知られているものを構築します。(基本単位が数と演算子である)数学表現と同じように、基本的な要素は単純なもの(文字、演算子、文字列、クラス、定義)ですが、それらを組み合わせることでより複雑な表現式を構築することができます。例えば、c は単一文字の正規表現で、c にマッチします。cc は2つの正規表現をつないだものを含む正規表現で、cc にマッチします。c* は、単一文字の正規表現 c と、それに続く演算子 * から構成される正規表現で、これは0個以上の c にマッチします。正規表現の真のパワーは、個々の要素よりもむしろ、それらを組み合わせる方法にあります。
下の表は、Flex で利用可能な正規表現をすべて示したものです。表中において、c は(エスケープ・シーケンスを含む)任意の単一文字を、r は任意の正規表現を、s は文字列を表します。表は、グループ別に編成されており、最も高い優先度を持つものが一番上にあります。
正規表現 | マッチの対象 | 例 |
---|---|---|
c | 特殊文字を除く任意の文字 | A または \n |
. | 改行(\n)を除く任意の文字 | efg.* |
[s] | クラス s 中にある任意の文字 | [efg] |
[^s] | クラス s 中にない任意の文字 | [^mnqs] |
r* | 0個以上の r | (a|[e-f])* |
r+ | 1個以上の r | (a|[e-f])+ |
r? | 0個または1個の r | (a|[e-f])? |
r{x,y} | x 個以上 y 個以下の r(abc{1,3} は ab のうしろに1個から3個までの c を付加したものに等しい) | abc{1,5} |
"s" | 字義どおりの文字列 s | "****" |
\c | (\c が ANSI C において特別な意味を持たない場合)c | \" または \* |
(r) | r - 丸括弧 () はグループ化のためのもの | (Ab|Bb) |
r1r2 | r1 のうしろに r2 が続くもの | Aa |
r1|r2 | r1 または r2 | A|B |
r1/r2 | r2 がうしろに続くという条件を満足する r1 | abc/123 |
^ | 行頭 | ^abc |
$ | 行末 | abc$ |
<start>r | スタート状態(start がアクティブな時 r がアクティブ) | <comment>"*/" |
<<EOF>> | ファイルの終端(End-Of-File ルールを参照) | <<EOF>> |
UNIX においてパターン検索が必要な場合は正規表現がよく使われますが、アプリケーションが異なると、正規表現もよく似てはいるもののまったく同一ではないという点に注意してください。例えば、Flex、egrep、Emacs はいずれもパターン検索のテンプレートとして正規表現を使いますが、それぞれが理解する正規表現は少しずつ異なります。特に、Flex では定義が使われますが、egrep や Emacs では使われませんし、egrep や Emacs は単語の先頭と末尾にマッチさせるための \< と \> とを提供していますが、Flex は提供していません。さらに、Emacs はバッファの先頭に対するマッチングや「ファジー」なマッチング等々を行うための特別な \letter シーケンスを他にも数多く提供しています。
何等かの条件に基づいてパターン・マッチング処理のルールを活性化することが便利な時があります。例えばいくつかのコンピュータ言語では、重複しているスキャン・ルールの曖昧さを取り除くのを支援するためにパース状態を使います。別の例としては、ある特定の入力が見つかったあとでだけ、あるルールを活性化したいという場合があります。このような状況に対処するために Flex はスタート条件あるいはスタート状態と呼ばれる簡単なシステムを提供しています。
スタート状態は、あるルールがアクティブになるのはいつであるかを Flex に通知するブーリアン値のようなものです。スタート状態は定義セクションにおいて(排他的スタート状態の場合)%x 、(包含的スタート状態の場合)%s を使って宣言されます。
これまでのところでは、Flex が異なる2種類のスタート状態をサポートしている事実から目をそらしてきました。2つのスタート状態とは包含的スタート状態(%s)と排他的スタート状態(%x)のことです。これら2つの相違点は、排他的スタート状態が活性化された場合はその状態に属するルールだけが活性化されるのに対して、包含的スタート状態の場合はその状態に属するルールとスタート状態への参照を持たないルールの両方が活性化されるという点にあります。この違いを示す例を挙げると、以下のようになります。
このことは、排他的スタート状態が使われる場合には、マッチされないテキストが stdout に出力されてはならないのであれば、可能なすべての入力にマッチするルールを個々の排他的スタート状態が持たなければならないということを意味しています。包含的スタート状態の場合は、あらゆる状態において有効な、スタート状態への参照を持たないルールを1つ持つ必要があります。
注:排他的スタート状態は POSIX の一部であるにもかかわらず、Lex はこれをサポートしていません。
スタート状態の名前を知っているだけではあまり役に立ちません。それらがいつ活性化されるのかを制御しなければなりません。活性化は、アクションの中、もしくは、記述情報内の追加的な C コードを記述する領域の中において BEGIN を使うことで実現されます。使い方は以下のとおりです。
上の例においては、定義されていない INITIAL という状態があることに注意してください。この状態は常に利用可能で、活性化された状態が1つも存在しない時のスキャナの初期状態を表します。このことは、BEGIN(INITIAL) によってスキャナの状態が効果的に(もちろん、その時点においてスキャンしている箇所を維持したまま)リセットされることを意味しています。
以下において、スタート状態の使用に関する注をいくつか示します。
1つのルールにおいては、単一のスタート状態、もしくは、カンマで区切られたスタート状態のリストのみ使用することができます。また、それらはルールの先頭になければなりません。次に示すものは正当です。
排他的スタート状態は、他のすべての状態を「無効」にするので、強力です。これは、スキャナの内部においてもう1つのスキャナを効果的に定義することができることを意味しています。これにより例えば、スタート状態次第で
C と Pascal の両方をスキャンするスキャナを定義することが理論的には可能になります。以下のようなコードが持つ効果を想像してみてください。
以前に述べたとおり、スタート状態はそれ自身の名前空間を持っていません。その理由は、スタート状態が
#define とほとんど同様の方法で整数値として定義されているからです。このことは、整数値と同様、スタート状態の「スタック」のようなものを作成することが可能であるということを意味しています。例えば、
プログラミングにおいて、何かをする方法を学ぶのに最良の方法は実際にそれをやってみることです。そのことを心にとめた上で、以下にスタート状態をどのように使うことができるかを示す実例を挙げます。
%{
#include <ctype.h>
char month[20],dow[20],day[20],year[20];
%}
skip of|the|[ \t,]* /* これらの文字の並びを無視する */
mon (mon(day)?) /* 曜日の名前の長い形式と短い形式のどちらにもマッチするよう設定する
*/
tue (tue(sday)?)
wed (wed(nesday)?)
thu (thu(rsday)?)
fri (fri(day)?)
sat (sat(urday)?)
sun (sun(day)?)
/* 以下は可能なすべての曜日を表す */
day_of_the_week ({mon}|{tue}|{wed}|{thu}|{fri}|{sat}|{sun})
jan (jan(uary)?) /* すべての月について同様のことを行う
*/
feb (feb(ruary)?)
mar (mar(ch)?)
apr (apr(il)?)
may (may)
jun (jun(e)?)
jul (jul(y)?)
aug (aug(ust)?)
sep (sep(tember)?)
oct (oct(ober)?)
nov (nov(ember)?)
dec (dec(ember)?)
/* 以下は可能なすべての月の名前を表す */
first_half
({jan}|{feb}|{mar}|{apr}|{may}|{jun})
second_half ({jul}|{aug}|{sep}|{oct}|{nov}|{dec})
month
{first_half}|{second_half}
/*
* 日、月、年の数値形式
* これらは重複しているため、正しくパースするには、スタート状態と日付の形式に関するある程度の知識が必要であることに注意
*/
nday [1-9]|[1-2][0-9]|3[0-1]
nmonth [1-9]|1[0-2]
nyear [0-9]{1,4}
/* 年と日のための拡張子 */
year_ext (ad|AD|bc|BC)?
day_ext (st|nd|rd|th)?
/*
* このプログラムの最後にあるルールを使ってすべての無関係なテキストを処理するために、非排他的なスタート状態を使う。こうしないと、個々のスタート状態のブロックにルールを追加しなければならなくなる。規模の大きいスキャナに対しては、これは実行可能なオプションであることが多い。何故なら、ルールの追加はスキャナのスピードに影響を与えないからである。ここでは、簡潔さを優先させた
*/
%s LONG SHORT
%s DAY MONTH /* 長い形式の日付のために追加した状態 */
%s YEAR_FIRST YEAR_LAST YFMONTH YLMONTH
%%
/*
* 曜日は常に最初に置かれて、うしろに続く日付の修飾子として機能するものと仮定される。よって、曜日は複数の日付形式の間で共用可能である
*/
<LONG>{day_of_the_week} strcpy(dow,yytext);
/*
* 月-日-年という形式の日付を処理する
* パース状態は LONG -> [月にマッチ] -> DAY -> LONG のように遷移する
*/
<LONG>{month}
strcpy(month,yytext); BEGIN(DAY);
<DAY>{nday}{day_ext}
strcpy(day,yytext); BEGIN(LONG);
/*
* 日-月-年という形式の日付を処理する
* パース状態は LONG -> [日にマッチ] -> MONTH -> LONG のように遷移する
*/
<LONG>{nday}{day_ext}
strcpy(day,yytext); BEGIN(MONTH);
<MONTH>{month}
strcpy(month,yytext); BEGIN(LONG);
/*
* 日付の年の部分は最後に置かれるものと考えられる。したがって、年を見つけたらパースされた日付を表示することができる。もちろん日付として不当なものであればゴミが出力されることになる
*/
<LONG>{nyear}{year_ext} {
<SHORT>{nday}
strcpy(day,yytext); BEGIN(YEAR_LAST);
<YEAR_LAST>{nmonth} strcpy(month,yytext);
BEGIN(YLMONTH);
<YLMONTH>{nyear}
strcpy(year,yytext); BEGIN(SHORT);
/*
* 年-月-日という形式の日付を処理する
* SHORT 状態で数値形式の年を見つけた場合は、日が日付の最後の部分になると仮定する
* パース状態は SHORT -> [年にマッチ] -> YEAR_FIRST -> YFMONTH -> SHORT
のように遷移する
*/
<SHORT>{nyear}
strcpy(year,yytext); BEGIN(YEAR_FIRST);
<YEAR_FIRST>{nmonth} strcpy(month,yytext);
BEGIN(YFMONTH);
<YFMONTH>{nday}
strcpy(day,yytext); BEGIN(SHORT);
/*
* 数値形式の日付では、年は最初になることも最後になることも可能。したがって、パースしたものをいつ表示すべきかを示すのに改行を使う
*/
<SHORT>\n {
long\n BEGIN(LONG);
short\n BEGIN(SHORT);
/*
* 以下のルールは、無関係なテキストを見つけて破棄する(マッチされたテキストはデフォルトでは
ECHO されないが、マッチされなかったテキストは ECHO される。ピリオドは改行以外のすべての文字を見つける。改行は
\n によって見つけられる)
*/
{skip}*
\n
.
このマニュアル中の他の実例と同様に、この実例も
Copyright (C) 1992, 1993 Free Software Foundation
Permission is granted to make and distribute verbatim copies of this manual provided the copyright notice and this permission notice are preserved on all copies.
Permission is granted to copy and distribute modified versions of this manual under the conditions for verbatim copying, provided that the entire resulting derived work is distributed under the terms of a permission notice identical to this one.
Permission is granted to copy and distribute translations of this manual into another language, under the above conditions for modified versions, except that this permission notice may be stated in a translation approved by the Free Software Foundation.
日本語訳:市川和久
Japanese translation by Kazuhisa Ichikawa (ki@home.email.ne.jp)